package com.example.musicplayer.gobang;

public class AI_4 {
    //num of threads to calculate
    private static final int THREADS = 21;
    //search depth
    private static final int DEPTH = 6;
    //mark means need to search
    private static final int MARKED = -2;
    //search range of location
    private static final int[] RANGE;
    private final int SIZE;
    private final Gobang mGobang;

    static {
        RANGE = new int[DEPTH];
        RANGE[0] = 63;
        RANGE[1] = 17;
        RANGE[2] = 17;
        RANGE[3] = 17;
        RANGE[4] = 17;
        RANGE[5] = 17;
    }

    private class AI_Thread extends Thread {
        private final int[][] mData;
        private final int[][] mLocation;
        private final int[] mEndFlag;
        private Location mResult;

        public AI_Thread(int[][] data, int[][] location, int[] endFlag) {
            mData = data;
            mLocation = location;
            mEndFlag = endFlag;
        }

        public Location getResult() {
            return mResult;
        }

        @Override
        public void run() {
            mResult = getLocation(mData, mLocation);
            synchronized (mEndFlag) {
                mEndFlag[0] += 1;
            }
        }
    }

    public AI_4(Gobang gobang) {
        mGobang = gobang;
        SIZE = mGobang.getChessBoardSize();
    }

    //external interface
    public Location nextLocation() {
        int[][] data = mGobang.copyChessLocation();
        Location location = checkFour(data, false);
        if (location != null) {
            return location;
        }
        location = checkFour(data, true);
        if (location != null) {
            return location;
        }
        int[][] checkLocation = updateScore(data, 0);

        if (THREADS > 1 && checkLocation.length >= THREADS) {
            int counts = checkLocation.length / THREADS;
            int lastCounts = checkLocation.length - counts * (THREADS - 1);
            int[][][] locations = new int[THREADS][][];
            for (int i = 0; i < THREADS; i++) {
                if (i == THREADS - 1) {
                    locations[i] = new int[lastCounts][2];
                } else {
                    locations[i] = new int[counts][2];
                }
                for (int j = 0; j < locations[i].length; j++) {
                    locations[i][j][0] = checkLocation[i * counts + j][0];
                    locations[i][j][1] = checkLocation[i * counts + j][1];
                }
            }
            int[] endFlag = new int[]{0};
            AI_Thread[] threads = new AI_Thread[THREADS];
            for (int k = 0; k < THREADS; k++) {
                threads[k] = new AI_Thread(mGobang.copyChessLocation(), locations[k], endFlag);
                threads[k].start();
            }
            //loop to wait all threads end
            while (endFlag[0] < THREADS) {
                try {
                    Thread.sleep(30);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            //find the max value
            Location resultLocation = threads[0].getResult();
            for (int k = 1; k < THREADS; k++) {
                if (threads[k].getResult().getEvaluate() > resultLocation.getEvaluate()) {
                    resultLocation = threads[k].getResult();
                }
            }
            return resultLocation;
        } else {
            return getLocation(data, checkLocation);
        }
    }

    /**
     * get the location that has the max evaluate
     *
     * @param data          chess data
     * @param checkLocation need check location array
     * @return the target location
     */
    private Location getLocation(int[][] data, int[][] checkLocation) {
        int round = mGobang.getRound();
        int[][] evaluate = new int[4][];
        initEvaluate(data, evaluate);

        int max = Integer.MIN_VALUE;
        int targetX = 0;
        int targetY = 0;
        int evaluateH = 0;
        int evaluateV = 0;
        int evaluateFS = 0;
        int evaluateBS = 0;
        int indexFS = 0;
        int indexBS = 0;
        for (int[] ints : checkLocation) {
            int x = ints[0];
            int y = ints[1];
            indexFS = x + y;
            indexBS = x - y + SIZE - 1;
            evaluateH = evaluate[0][x];
            evaluateV = evaluate[1][y];
            evaluateFS = evaluate[2][indexFS];
            evaluateBS = evaluate[3][indexBS];
            data[x][y] = round;
            evaluate[0][x] = evaluateH(data, SIZE, round, x);
            evaluate[1][y] = evaluateV(data, SIZE, round, y);
            evaluate[2][indexFS] = evaluateFS(data, SIZE, round, x, y);
            evaluate[3][indexBS] = evaluateBS(data, SIZE, round, x, y);
            int value = MaxMin(data, evaluate, x, y, Gobang.getOppositeRound(round), Integer.MIN_VALUE, Integer.MAX_VALUE, 1);
            data[x][y] = Gobang.EMPTY;
            evaluate[0][x] = evaluateH;
            evaluate[1][y] = evaluateV;
            evaluate[2][indexFS] = evaluateFS;
            evaluate[3][indexBS] = evaluateBS;
            if (value > max) {
                max = value;
                targetX = x;
                targetY = y;
            }
        }
        Location location = new Location(targetX, targetY);
        location.setEvaluate(max);
        return location;
    }

    /**
     * the max-min search
     *
     * @param data     the chess data
     * @param evaluate evaluate info
     * @param x        last chess x
     * @param y        last chess y
     * @param round    current round
     * @param alpha    the max layer
     * @param beta     the min layer
     * @param depth    current search depth
     * @return the optimal value
     */
    private int MaxMin(int[][] data, int[][] evaluate, int x, int y, int round, int alpha, int beta, int depth) {
        if (depth >= DEPTH || Gobang.isWin(data, x, y, Gobang.getOppositeRound(round))) {
            return evaluate(evaluate);
        }
        int[][] checkLocation = updateScore(data, depth);
        int thisRound = mGobang.getRound();
        int evaluateH = 0;
        int evaluateV = 0;
        int evaluateFS = 0;
        int evaluateBS = 0;
        int indexFS = 0;
        int indexBS = 0;
        for (int[] ints : checkLocation) {
            int checkX = ints[0];
            int checkY = ints[1];
            indexFS = checkX + checkY;
            indexBS = checkX - checkY + SIZE - 1;
            evaluateH = evaluate[0][checkX];
            evaluateV = evaluate[1][checkY];
            evaluateFS = evaluate[2][indexFS];
            evaluateBS = evaluate[3][indexBS];
            data[checkX][checkY] = round;
            evaluate[0][checkX] = evaluateH(data, SIZE, thisRound, checkX);
            evaluate[1][checkY] = evaluateV(data, SIZE, thisRound, checkY);
            evaluate[2][indexFS] = evaluateFS(data, SIZE, thisRound, checkX, checkY);
            evaluate[3][indexBS] = evaluateBS(data, SIZE, thisRound, checkX, checkY);
            int value = MaxMin(data, evaluate, checkX, checkY, Gobang.getOppositeRound(round), alpha, beta, depth + 1);
            data[checkX][checkY] = Gobang.EMPTY;
            evaluate[0][checkX] = evaluateH;
            evaluate[1][checkY] = evaluateV;
            evaluate[2][indexFS] = evaluateFS;
            evaluate[3][indexBS] = evaluateBS;
            //if our round, get the max value. If opposite round, get the min value
            if (round == mGobang.getRound()) {
                alpha = Math.max(alpha, value);
            } else {
                beta = Math.min(beta, value);
            }
            //alpha-beta cut
            if (alpha >= beta) {
                if (round == mGobang.getRound()) {
                    return alpha;
                } else {
                    return beta;
                }
            }
        }
        if (round == mGobang.getRound()) {
            return alpha;
        }
        return beta;
    }

    //get each location's score
    private int[][] updateScore(int[][] data, int depth) {
        int[][] scores = new int[SIZE][SIZE];
        AI_2.calculateScore(data, scores, SIZE);
        int[][] needCheckLocation;
        if (mGobang.getCounts() + depth >= SIZE * SIZE - RANGE[depth]) {
            needCheckLocation = new int[SIZE * SIZE - mGobang.getCounts() - depth][2];
            int index = 0;
            for (int i = 0; i < SIZE; i++) {
                for (int j = 0; j < SIZE; j++) {
                    if (data[i][j] == Gobang.EMPTY) {
                        needCheckLocation[index][0] = i;
                        needCheckLocation[index][1] = j;
                        index++;
                    }
                }
            }
        } else {
            needCheckLocation = new int[RANGE[depth]][2];
            int max = -1;
            int x = 0;
            int y = 0;
            int k = 1;
            while (k <= RANGE[depth]) {
                for (int i = 0; i < SIZE; i++) {
                    for (int j = 0; j < SIZE; j++) {
                        if (scores[i][j] > max) {
                            max = scores[i][j];
                            x = i;
                            y = j;
                        }
                    }
                }
                scores[x][y] = MARKED;
                needCheckLocation[k - 1][0] = x;
                needCheckLocation[k - 1][1] = y;
                max = -1;
                x = 0;
                y = 0;
                k++;
            }
        }
        return needCheckLocation;
    }

    private void initEvaluate(int[][] data, int[][] evaluate) {
        evaluate[0] = new int[SIZE];
        for (int i = 0; i < SIZE; i++) {
            evaluate[0][i] = evaluateH(data, SIZE, mGobang.getRound(), i);
        }
        evaluate[1] = new int[SIZE];
        for (int i = 0; i < SIZE; i++) {
            evaluate[1][i] = evaluateV(data, SIZE, mGobang.getRound(), i);
        }
        evaluate[2] = new int[SIZE + SIZE - 1];
        for (int i = 0; i < SIZE; i++) {
            evaluate[2][i] = evaluateFS(data, SIZE, mGobang.getRound(), i, 0);
        }
        for (int j = 1; j < SIZE; j++) {
            evaluate[2][SIZE - 1 + j] = evaluateFS(data, SIZE, mGobang.getRound(), SIZE - 1, j);
        }
        evaluate[3] = new int[SIZE + SIZE - 1];
        for (int j = SIZE - 1; j >= 0; j--) {
            evaluate[3][SIZE - 1 - j] = evaluateBS(data, SIZE, mGobang.getRound(), 0, j);
        }
        for (int i = 1; i < SIZE; i++) {
            evaluate[3][SIZE - 1 + i] = evaluateBS(data, SIZE, mGobang.getRound(), i, 0);
        }
    }

    /**
     * check if has four in a row
     *
     * @param data          the chess data
     * @param checkOpposite if check opposite chess
     * @return null if no four in a row, the location if has four
     */
    private Location checkFour(int[][] data, boolean checkOpposite) {
        if (mGobang.getCounts() >= 7) {
            int preX = mGobang.getLocX(mGobang.getCounts() - 1);
            int preY = mGobang.getLocY(mGobang.getCounts() - 1);
            if (!checkOpposite) {
                preX = mGobang.getLocX(mGobang.getCounts() - 2);
                preY = mGobang.getLocY(mGobang.getCounts() - 2);
            }
            int checkChess = checkOpposite ? mGobang.getOppositeRound() : mGobang.getRound();
            int tempX;
            int tempY;
            //horizontal direction
            for (int i = -4; i <= 4; i++) {
                tempX = preX;
                tempY = preY + i;
                if (tempY >= 0 && tempY < SIZE && mGobang.getChess(tempX, tempY) == Gobang.EMPTY) {
                    data[tempX][tempY] = checkChess;
                    if (Gobang.isWin(data, tempX, tempY, checkChess)) {
                        data[tempX][tempY] = Gobang.EMPTY;
                        return new Location(tempX, tempY);
                    }
                    data[tempX][tempY] = Gobang.EMPTY;
                }
            }
            //vertical direction
            for (int i = -4; i <= 4; i++) {
                tempX = preX + i;
                tempY = preY;
                if (tempX >= 0 && tempX < SIZE && mGobang.getChess(tempX, tempY) == Gobang.EMPTY) {
                    data[tempX][tempY] = checkChess;
                    if (Gobang.isWin(data, tempX, tempY, checkChess)) {
                        data[tempX][tempY] = Gobang.EMPTY;
                        return new Location(tempX, tempY);
                    }
                    data[tempX][tempY] = Gobang.EMPTY;
                }
            }
            //forward slash direction
            for (int i = -4; i <= 4; i++) {
                tempX = preX + i;
                tempY = preY - i;
                if (tempX >= 0 && tempX < SIZE && tempY >= 0 && tempY < SIZE && mGobang.getChess(tempX, tempY) == Gobang.EMPTY) {
                    data[tempX][tempY] = checkChess;
                    if (Gobang.isWin(data, tempX, tempY, checkChess)) {
                        data[tempX][tempY] = Gobang.EMPTY;
                        return new Location(tempX, tempY);
                    }
                    data[tempX][tempY] = Gobang.EMPTY;
                }
            }
            //back slash direction
            for (int i = -4; i <= 4; i++) {
                tempX = preX - i;
                tempY = preY - i;
                if (tempX >= 0 && tempX < SIZE && tempY >= 0 && tempY < SIZE && mGobang.getChess(tempX, tempY) == Gobang.EMPTY) {
                    data[tempX][tempY] = checkChess;
                    if (Gobang.isWin(data, tempX, tempY, checkChess)) {
                        data[tempX][tempY] = Gobang.EMPTY;
                        return new Location(tempX, tempY);
                    }
                    data[tempX][tempY] = Gobang.EMPTY;
                }
            }
        }
        return null;
    }

    private int evaluate(int[][] evaluate) {
        int evaluateScore = 0;
        for (int[] ints : evaluate) {
            for (int anInt : ints) {
                evaluateScore += anInt;
            }
        }
        return evaluateScore;
    }

    public static int evaluateH(int[][] data, int size, int chess, int i) {
        int ourScore = 0;
        int oppositeScore = 0;
        //horizontal
        ourScore += getValue(data[i], size, chess);
        oppositeScore += getValue(data[i], size, Gobang.getOppositeRound(chess));
        return ourScore - oppositeScore;
    }

    public static int evaluateV(int[][] data, int size, int chess, int j) {
        int ourScore = 0;
        int oppositeScore = 0;
        int[] temp = new int[size];
        //vertical
        for (int k = 0; k < size; k++) {
            temp[k] = data[k][j];
        }
        ourScore += getValue(temp, size, chess);
        oppositeScore += getValue(temp, size, Gobang.getOppositeRound(chess));
        return ourScore - oppositeScore;
    }

    public static int evaluateFS(int[][] data, int size, int chess, int i, int j) {
        int ourScore = 0;
        int oppositeScore = 0;
        int[] temp = new int[size];
        int length = 0;
        int ij = i + j;
        //forward slash, the above part
        if (ij < size) {
            length = ij;
            for (int k = 0; k < length; k++) {
                temp[k] = data[ij - k][k];
            }
        } else {
            length = size + size - ij - 1;
            for (int k = 0; k < length; k++) {
                temp[k] = data[size - 1 - k][ij + k + 1 - size];
            }
        }
        ourScore += getValue(temp, length, chess);
        oppositeScore += getValue(temp, length, Gobang.getOppositeRound(chess));
        return ourScore - oppositeScore;
    }

    public static int evaluateBS(int[][] data, int size, int chess, int i, int j) {
        int ourScore = 0;
        int oppositeScore = 0;
        int[] temp = new int[size];
        int length = 0;
        int ij = i - j;
        //back slash, the below part
        if (ij >= 0) {
            length = size - ij;
            for (int k = 0; k < length; k++) {
                temp[k] = data[ij + k][k];
            }
        } else {
            length = size + ij;
            for (int k = 0; k < length; k++) {
                temp[k] = data[k][k - ij];
            }
        }
        ourScore += getValue(temp, length, chess);
        oppositeScore += getValue(temp, length, Gobang.getOppositeRound(chess));
        return ourScore - oppositeScore;
    }

    //get the value of chess, size to check, color is chess
    private static int getValue(int[] arr, int size, int chess) {
        //the value
        int value = 0;
        //connected chess num
        int counts = 0;
        //the empty loc before and after connected chessmen
        int empty = 0;
        int i = 1;
        if (arr[0] == Gobang.EMPTY) {
            empty++;
        } else if (arr[0] == chess) {
            counts++;
        }
        while (i < size) {
            if (arr[i] == chess) {
                counts++;
            } else if (arr[i] == Gobang.EMPTY) {
                if (counts == 0) {
                    empty = 1;
                } else {
                    value += ScoreTable.getScore(counts, empty + 1);
                    empty = 1;
                    counts = 0;
                }
            } else {
                value += ScoreTable.getScore(counts, empty);
                empty = 0;
                counts = 0;
            }
            i++;
        }
        value += ScoreTable.getScore(counts, empty);
        return value;
    }

    private static class ScoreTable {
        //A-B, A means the num of connected chessmen，B means two sides' empty location's num
        public static final int FIVE = 100000000;//100000;
        public static final int FOUR_ZERO = 10000000;//10000;
        public static final int FOUR_ONE = 1000000;//2000;
        public static final int THREE_ZERO = 100000;//1500;
        public static final int THREE_ONE = 10000;//100;
        public static final int TWO_ZERO = 1000;//100;
        public static final int TWO_ONE = 100;//10;
        public static final int ONE_ZERO = 10;
        public static final int ONE_ONE = 1;
        public static final int OTHER = 0;

        /**
         * get score
         *
         * @param counts num of connected chessmen
         * @param empty  empty location in the two sides
         * @return the score
         */
        public static int getScore(int counts, int empty) {
            if (counts >= 5) {
                return FIVE;
            } else if (counts == 4) {
                if (empty == 2) {
                    return FOUR_ZERO;
                } else if (empty == 1) {
                    return FOUR_ONE;
                }
            } else if (counts == 3) {
                if (empty == 2) {
                    return THREE_ZERO;
                } else if (empty == 1) {
                    return THREE_ONE;
                }
            } else if (counts == 2) {
                if (empty == 2) {
                    return TWO_ZERO;
                } else if (empty == 1) {
                    return TWO_ONE;
                }
            } else if (counts == 1) {
                if (empty == 2) {
                    return ONE_ZERO;
                } else if (empty == 1) {
                    return ONE_ONE;
                }
            }
            return OTHER;
        }
    }
}